import axios from 'axios'
import * as qiniu from 'qiniu-js'
import { isArray } from '@/utils'
import { intercept } from '@/utils/request/interceptor'
import { handleError } from '@/utils/request/error'
import { Api, JBoltApiType, ResData } from '@/typings/request'
import { transformResultByErrorStatus } from '@/utils/request/transform'
import { CONCURRENT_REQUEST_ERROR_CODE } from '@/utils/request/constant'
import { showErrorMsg, showSuccessMsg } from '@/utils/request/msg'
import { generatePath } from '@/utils/common/file'
import { getServiceEnvConfig } from '~/.env-config'

const { url } = getServiceEnvConfig(import.meta.env)

const instance = axios.create({
    baseURL: url,
    timeout: 10000,
    withCredentials: false
})

intercept(instance)

/**
 * 限制并发的请求路径
 */
const concurrentRequestKey = new Set<string>()

/**
 * 请求
 * @param method
 * @param url
 * @param data
 * @param config
 */
async function request<T = ResData>(
    url: string,
    data: any = null,
    config: Api.RequestConfig = {}
): Promise<Api.Result<T>> {
    let result: Api.Result = {} as unknown as Api.Result
    try {
        if (config?.key) {
            // 如果有key，检测是否有相同key的请求正在执行
            if (concurrentRequestKey.has(config.key)) {
                console.warn('并发请求被阻止', config.key)
                return transformResultByErrorStatus(CONCURRENT_REQUEST_ERROR_CODE)
            }
            concurrentRequestKey.add(config.key)
        }
        // eslint-disable-next-line @typescript-eslint/no-unused-vars

        config.url = url
        if (data !== null && data !== undefined) {
            config.data = data
        }

        result = (await instance.request(config)) as unknown as Api.Result
        return result
    } catch (error) {
        //4xx 5xx开头的响应会直接进入这里，不走拦截器
        result = handleError(error)

        return result
    } finally {
        if (config?.key) {
            concurrentRequestKey.delete(config.key)
        }
        if (result?.error) {
            showErrorMsg(result.error, config)
        } else if (result?.result?.msg) {
            showSuccessMsg(result.result.msg, config)
        }
    }
}
/**
 * get请求
 * @param url - 请求地址
 * @param data - 请求的参数，会拼接到url上
 * @param config - axios配置
 */
function get<T = ResData>(
    url: string,
    data?: any,
    config?: Api.RequestConfig
): Promise<Api.Result<T>> {
    return request<T>(url, null, {
        method: 'get',
        params: data,
        ...config
    })
}
/**
 * get请求
 * @param url - 请求地址
 * @param data - 数据
 * @param config - axios配置
 */
async function tryGet<T = ResData>(
    url: string,
    data: any = null,
    config: Api.RequestConfig = {}
): Promise<T> {
    const result = await get<T>(url, data, config)
    if (!result.error) {
        return Promise.resolve(result.result)
    }

    return Promise.reject(result)
}

/**
 * post请求
 * @param url - 请求地址
 * @param data - 请求的body的data
 * @param config - axios配置
 */
function post<T = ResData>(
    url: string,
    data?: any,
    config?: Api.RequestConfig
): Promise<Api.Result<T>> {
    return request<T>(url, data, {
        method: 'post',
        ...config
    })
}
/**
 * put请求
 * @param url - 请求地址
 * @param data - 请求的body的data
 * @param config - axios配置
 */
function put<T = ResData>(
    url: string,
    data?: any,
    config?: Api.RequestConfig
): Promise<Api.Result<T>> {
    return request<T>(url, data, {
        method: 'put',
        ...config
    })
}

/**
 * post请求
 * @param url - 请求地址
 * @param data - 请求的body的data
 * @param config - axios配置
 */
async function tryPost<T = ResData>(
    url: string,
    data?: any,
    config?: Api.RequestConfig
): Promise<T> {
    const result = await request<T>(url, data, {
        method: 'post',
        ...config
    })
    if (!result.error) {
        return Promise.resolve(result.result)
    }

    return Promise.reject(result)
}

/**
 * 七牛token的缓存，30s内不会重新请求，基本控制在一次操作以内，时间太长，后台修改配置了前端也不知道，导致很长一段时间的文件上传可能都有问题。
 */
const qiniuDomainTokenCache = new Map<string, { expireTime: number; data: any }>()

/**
 * 获取七牛上传的token以及域名
 * @param sn
 */
async function getQiniuDomainToken(sn?: string) {
    const cache = qiniuDomainTokenCache.get(sn ?? '')
    if (cache && cache.expireTime > Date.now()) {
        return cache.data
    }
    const res = await tryGet<ResData>(`/api/admin/qiniu/getUploadParas?sn=${sn ?? ''}`)
    qiniuDomainTokenCache.set(sn ?? '', {
        expireTime: Date.now() + 30000,
        data: res.data
    })
    return res.data
}

/**
 * 上传到七牛
 * @param files 要上传的文件，可以使单个，也可以是file数组
 * @param qiniuPath 七牛保存路径，支持动态参数。参考src/utils/common/file.ts中generatePath方法说明
 * @param qiniuSn 使用哪个七牛账号，不传则使用默认账号，但必须后台设置了默认账号
 * @param next 上传中的回调，(size: number, loaded: number, percent: number) => void
 * @param complete 上传完成的回调 (url: string, key: string, hash: string) => void
 * @param error 上传失败的回调 (file: File, path: string, error: Error) => void
 * @param putExtra 参考七牛文档 https://developer.qiniu.com/kodo/1283/javascript#usage
 * @param config 参考七牛文档 https://developer.qiniu.com/kodo/1283/javascript#usage
 */
async function uploadToQiniu(
    files: File[] | File,
    qiniuPath: string,
    qiniuSn?: string,
    next?: (args: { size: number; loaded: number; percent: number }) => void,
    complete?: (args: { url: string; key: string; hash: string }) => void,
    error?: (args: { file: File; path: string; error: Error }) => void,
    putExtra?: Record<string, any>,
    config?: Record<string, any>
): Promise<string[] | string> {
    const { domain, token } = await getQiniuDomainToken(qiniuSn)
    if (isArray(files)) {
        return Promise.all(
            files.map((file) =>
                uploadToQiniu(
                    file,
                    qiniuPath,
                    qiniuSn,
                    next,
                    complete,
                    error,
                    putExtra,
                    config
                )
            )
        ) as Promise<string[]>
    }
    return new Promise((resolve, reject) => {
        let path = generatePath(files.name, qiniuPath)
        if (path.startsWith('/')) path = path.substring(1) //七牛上传路径不能以/开头
        qiniu.upload(files, path, token, putExtra, config).subscribe({
            next: (res) => {
                if (next) {
                    next({
                        size: res.total.size,
                        loaded: res.total.loaded,
                        percent: res.total.percent
                    })
                }
            },
            error: (err) => {
                if (error) {
                    error({ file: files, path, error: err })
                }
                reject(err)
            },
            complete: (res) => {
                if (complete) {
                    complete({
                        url: domain + '/' + res.key,
                        key: res.key,
                        hash: res.hash
                    })
                }
                resolve(domain + '/' + res.key)
            }
        })
    })
}

/**
 * 上传到服务器，需要在.env-config.ts中配置serverUploadPrefix
 * @param files 要上传的文件，可以使单个，也可以是file数组
 * @param savePath 服务器保存路径
 * @param next
 * @param complete
 * @param error
 */
async function uploadToServer(
    files: File[] | File,
    savePath: string,
    next?: (args: { size: number; loaded: number; percent: number }) => void,
    complete?: (args: { url: string; id: string }) => void,
    error?: (args: { file: File; path: string; error: Error }) => void
): Promise<string[] | string> {
    const { serverUploadApi } = getServiceEnvConfig(import.meta.env)
    if (!serverUploadApi) {
        throw new Error('请在.env-config.ts中配置serverUploadApi后再使用该方法')
    }
    if (isArray(files)) {
        return Promise.all(
            files.map((file) => uploadToServer(file, savePath, next, complete, error))
        ) as Promise<string[]>
    }
    return new Promise((resolve, reject) => {
        let path = generatePath(files.name, savePath)
        if (path.startsWith('/')) path = path.substring(1) //不能以/开头
        tryPost<ResData>(
            `${serverUploadApi}?path=${path}`,
            { file: files },
            {
                headers: {
                    'Content-Type': 'multipart/form-data'
                },
                onUploadProgress: function (progressEvent) {
                    // 处理原生进度事件
                    // 打印 progressEvent 对象查看属性
                    // 获取两个重要参数，计算上传进度，
                    // 最后保留1位小数
                    if (next) {
                        const progress = progressEvent.total
                            ? (progressEvent.loaded / progressEvent.total) * 100
                            : 0
                        next({
                            size: progressEvent.total ?? 0,
                            loaded: progressEvent.loaded,
                            percent: progress
                        })
                    }
                }
            }
        )
            .then((res) => {
                if (complete) {
                    complete({ url: res.data.url!, id: res.data.id })
                }
                resolve(res.data.url)
            })
            .catch((err) => {
                if (error) {
                    error({ file: files, path, error: err })
                }
                reject(err)
            })
    })
}

/**
 * 下载文件
 * @param url
 * @param filename
 */
async function download(
    url: string,
    filename: string,
    data?: any,
    config?: Api.RequestConfig
) {
    const res = await tryGet<Blob>(url, data, {
        responseType: 'blob',
        skipResponseInterceptor: true,
        onResponse(response) {
            if (!filename && response?.headers['content-disposition']) {
                const contentDisposition = response.headers['content-disposition']
                const match = contentDisposition.match(/filename=(.+)/)
                if (match) {
                    filename = decodeURIComponent(match[1])
                }
            }
        },
        ...config
    })
    const blob = new Blob([res], { type: 'application/octet-stream' })
    const link = document.createElement('a')
    link.href = URL.createObjectURL(blob)
    if (!filename) {
        filename = url.substring(
            url.lastIndexOf('/') + 1,
            url.includes('?') ? url.indexOf('?') : url.length
        )
    }
    link.download = filename
    link.click()
    URL.revokeObjectURL(link.href)
}
export const JBoltApi: JBoltApiType = {
    get,
    tryGet,
    post,
    tryPost,
    uploadToQiniu,
    uploadToServer,
    put,
    download
}
